#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import re
import time
import shlex
import psutil
import signal
import pathlib
import asyncio
import platform
import subprocess
from .util import WindowsRegistryFileReader
from ._util import Util
from ._const import Const
from ._errors import WinePrefixError
from ._reg import WineReg, AlternativeReg
from ._winecfg import WineConfigurationEditor
from ._winetricks import Winetricks


class WinePrefix:

    def __init__(self, path: str, winearch: str, singleton: bool = False, verbose: int = 0):
        '''
        This object represents a Wine prefix directory.

        Parameters
        ----------
        path: str
            full path to your wineprefix
        winearch: str
            the Arch to be setted:
            "win32" - 32-bit wine prefix
            "win64" - 64-bit wine prefix
        '''

        assert os.path.isabs(path)
        assert winearch in Const.archList and self._check_arch_compatibility(winearch)
        assert verbose in Const.verboseLevelList

        self._path = path
        self._arch = winearch
        self._verbose = verbose

        if not os.path.exists(self._path):
            # wineboot can create directory itself, but self._execute() needs a valid cwd
            # directory content will be filled when the first wine program executes
            os.mkdir(self._path)
        else:
            self._check_wineprefix(self._arch, self._path)

        self._wineTmpDir = None
        if True:
            st = os.stat(self._path)
            self._wineTmpDir = os.path.join("/tmp", ".wine-%d" % (os.getuid()), "server-%x-%x" % (st.st_dev, st.st_ino))

        if singleton:
            envs = os.environ.copy()
            envs["WINEPREFIX"] = self._path

            # use "-p 9999" to make wineserver do not auto stop
            self._wineServerProc = subprocess.Popen(["wineserver", "-f", "-p", "9999"], env=envs, cwd=self._path)

            # wait and check wineserver lock file and socket file, to be sure that wineserver has run to a ready state
            # it is also a singleton check
            psProc = psutil.Process(self._wineServerProc.pid)
            sockFullfn = os.path.join(self._wineTmpDir, "socket")
            lockFullfn = os.path.join(self._wineTmpDir, "lock")
            while True:
                time.sleep(0.1)

                if os.path.exists(sockFullfn) and os.path.exists(lockFullfn):
                    try:
                        if any([x.path == lockFullfn for x in psProc.open_files()]):
                            break
                    except psutil.NoSuchProcess:
                        pass
                    except psutil.AccessDenied:
                        # FIXME: self._wineServerProc is my own process, why this error occurs?
                        pass

                if self._wineServerProc.poll() is not None:
                    if self._wineServerProc.returncode == 2:
                        raise WinePrefixError("wine prefix is occupied by others")
                    else:
                        raise WinePrefixError("wine server terminates unexpectedly (return code: %d)" % (self._wineServerProc.returncode))
        else:
            self._wineServerProc = None

        if not os.path.exists(os.path.join(self._path, "drive_c")):
            self._execute(["wineboot", "-i"])

        self._objReg = WineReg(self)
        self._objAltReg = AlternativeReg(self)
        self._objWinecfg = WineConfigurationEditor(self)
        self._objWinetricks = Winetricks(self)

    @property
    def path(self):
        return self._path

    @property
    def winearch(self):
        return self._arch

    def get_wine_reg(self):
        return self._objReg

    def get_alternative_reg(self):
        return self._objAltReg

    def get_winecfg(self):
        return self._objWinecfg

    def get_winetricks(self):
        return self._objWinetricks

    def run(self, command, cwd=None, env=None, shell=False, cpu_cores=None):
        if not shell:
            command = ["wine"] + command
        else:
            command = "wine " + command
        self._execute(command, cwd=cwd, env=env, shell=shell, cpuCores=cpu_cores)        # here bFakeX and printRegFile are not supported

    async def async_run(self, command, cwd=None, env=None, cpu_cores=None):
        await self._asyncExecute("wine", command, cwd=cwd, env=env, cpuCores=cpu_cores)

    def restart(self):
        '''
        Simulate system restart for the wineprefix,
        don't do normal startup operations.
        '''
        self._execute(["wineboot", "-r"], bNoGUI=True)

    def shutdown(self):
        '''
        Simulate system shutdown for the wineprefix, don't reboot.
        '''
        self._execute(["wineboot", "-s"], bNoGUI=True)

    # FIXME: wineserver can occupy this directory
    # def wait_stop(self):
    #     while self._check_used():
    #         time.sleep(1.0)

    def prepare_envs(self, dll_overrides={}):
        assert "WINEPREFIX" not in os.environ
        assert "WINEARCH" not in os.environ
        assert "WINEDEBUG" not in os.environ
        assert "WINEDLLOVERRIDES" not in os.environ

        envs = os.environ.copy()

        envs["WINEPREFIX"] = self._path
        envs["WINEARCH"] = self._arch
        envs["WINEDEBUG"] = Const.verboseLevelList[self._verbose]

        if len(dll_overrides) > 0:
            tlist = []
            for name, override in dll_overrides.items():
                assert name not in Const.noOverrideDllList
                if override in ["forbidden", ""]:
                    tlist.append(f"{name}=")
                else:
                    override = Const.dllOverrideValueDict.get(override, override)
                    assert override in Const.dllOverrideValueDict.values()
                    tlist.append(f"{name}={override}")
            envs["WINEDLLOVERRIDES"] = ";".join(tlist)

        return envs

    def dispose(self):
        self._objWinetricks.dispose()
        del self._objWinetricks

        self._objWinecfg.dispose()
        del self._objWinecfg

        self._objAltReg.dispose()
        del self._objAltReg

        self._objReg.dispose()
        del self._objReg

        if self._wineServerProc is not None:
            assert self._wineServerProc.poll() is None
            self._wineServerProc.send_signal(signal.SIGINT)     # SIGINT makes wineserver also terminate auxillary processes
            self._wineServerProc.wait()
            del self._wineServerProc

    def _check_arch_compatibility(self, arch):
        '''
        Check if the given wine arch is compatible with the running system
        TODO: this method should check for ARM wine compatibility on ARM devices
        '''

        cpuArch = platform.machine()
        if cpuArch == "i386":
            if arch == "win32":
                return True
            if arch == "win64":
                return False
        if cpuArch == "x86_64":
            if arch == "win32":
                return True
            if arch == "win64":
                return True
        assert False

    @staticmethod
    def _check_wineprefix(arch, wineprefix):
        '''
        Check if the given wine arch is compatible with the running system
        '''

        if len(os.listdir(wineprefix)) > 0:
            p1 = os.path.join(wineprefix, "drive_c", "Program Files")
            p2 = os.path.join(wineprefix, "drive_c", "Program Files (x86)")
            if arch == "win32":
                if not (os.path.exists(p1) and not os.path.exists(p2)):
                    raise WinePrefixError("incompatible wineprefix and winearch")
            elif arch == "win64":
                if not (os.path.exists(p1) and os.path.exists(p2)):
                    raise WinePrefixError("incompatible wineprefix and winearch")
            else:
                assert False

    def _check_used(self):
        '''
        Get processes running on the wineprefix.

        Return
        ------
        boolean:
            Returns True if used.
        '''

        winedbg = self._execute(["winedbg", "--command", "info proc"], shell=False, bNoGUI=False).split("\n")

        # remove the first line from the output (the header)
        del winedbg[0]

        for w in winedbg:
            w = re.sub(r"\s{2,}", " ", w)[1:].replace("'", "")
            if r"\_" in w:
                w = w.replace(r"\_ ", "")
                w += " child"
            w = w.split(" ")
            if len(w) >= 3 and w[1].isdigit():
                return True

        return False

    def _execute(self, command, cwd=None, env=None, shell=False, cpuCores=None, bNoGUI=False, printRegFile=None):
        '''
        Execute command inside wineprefix

        Parameters
        ----------
        command: str
            command to be executed inside the wineprefix
        cwd: str, optional
            if speicified, the command will be executed using "cwd" as working directory
            default working directory is the Wine prefix directory (not C:)
        env: str, optional
            if speicified, the command will be executed using "env" as environment variables
            default environment variables are the return value of self.prepare_envs(), it contains all environment variables from parent process
            "env" should also be based on the return value of self.prepare_envs()
        shell: bool, optional
            if true, the command will be executed through the shell
        cpuCores: int, optional
            if specified, the command will be executed in only specified cpu core numbers
        bNoGUI: bool, optional
            if true, hide any GUI window
        printRegFile: bool, optional
            if specified, the file content would be printed, for debugging only
        '''

        if cwd is None:
            cwd = self._path

        if env is None:
            env = self.prepare_envs()

        tempOutFullfn = os.path.join(self._path, "temp.out")
        try:
            if self._verbose > 0:
                if not shell:
                    print(shlex.join(command))
                else:
                    print(command)
                if printRegFile is not None:
                    print(printRegFile + ": \"\"\"")
                    print(WindowsRegistryFileReader(printRegFile).get_text())
                    print("\"\"\"")

            outStr = None
            if self._wineServerProc is None:
                if bNoGUI:
                    # FIXME
                    # with xvfbwrapper.Xvfb(640, 480, 24):
                    proc = Util.procCreate(cpuCores, command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env, cwd=cwd, shell=shell)
                    outStr, _ = Util.procCommunicate(proc)
                else:
                    proc = Util.procCreate(cpuCores, command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env, cwd=cwd, shell=shell)
                    outStr, _ = Util.procCommunicate(proc)
            else:
                # wine app would stuck if either stdout=subprocess.PIPE or stderr=subprocess.PIPE is used and "wineserver -f" is running
                # we have to use a temp file to store stdout
                # it is very strange
                if not shell:
                    command = shlex.join(command)
                if bNoGUI:
                    # FIXME
                    # with xvfbwrapper.Xvfb(640, 480, 24):
                    proc = Util.procCreate(cpuCores, f"{command} > {tempOutFullfn} 2> {tempOutFullfn}", env=env, cwd=cwd, shell=True)
                    Util.procWait(proc)
                else:
                    proc = Util.procCreate(cpuCores, f"{command} > {tempOutFullfn} 2> {tempOutFullfn}", env=env, cwd=cwd, shell=True)
                    Util.procWait(proc)
                outStr = pathlib.Path(tempOutFullfn).read_text()

            if self._verbose > 0:
                print(outStr)

            return outStr
        except subprocess.CalledProcessError as e:
            if self._wineServerProc is not None:
                # see above, it is very strange
                e.stdout = pathlib.Path(tempOutFullfn).read_text()

            if self._verbose > 0:
                print(e.stdout)

            raise

    async def _asyncExecute(self, *command, cwd=None, env=None, cpuCores=None):
        '''
        Execute command inside wineprefix (using asyncio framework)

        Parameters
        ----------
        command: str
            command to be executed inside the wineprefix
        cwd: str, optional
            if speicified, the command will be executed using "cwd" as working directory
            default working directory is the Wine prefix directory (not C:)
        env: str, optional
            if speicified, the command will be executed using "env" as environment variables
            default environment variables are the return value of self.prepare_envs(), it contains all environment variables from parent process
            "env" should also be based on the return value of self.prepare_envs()
        cpuCores: int, optional
            if specified, the command will be executed in only specified cpu core numbers
        '''

        if cwd is None:
            cwd = self._path

        if env is None:
            env = self.prepare_envs()

        if self._verbose > 0:
            print(shlex.join(command))

        try:
            outStr = None
            if self._wineServerProc is None:
                proc = await Util.asyncProcCreateExec(cpuCores, *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, cwd=cwd, env=env)
                outStr, _ = await Util.asyncProcWait(proc, command)
            else:
                # wine app would stuck if either stdout=subprocess.PIPE or stderr=subprocess.PIPE is used and "wineserver -f" is running
                # we have to use a temp file to store stdout
                # it is very strange
                tempOutFullfn = os.path.join(self._path, "temp.out")
                cmd = f"{shlex.join(command)} > {tempOutFullfn} 2> {tempOutFullfn}"
                proc = await Util.asyncProcCreateShell(cpuCores, cmd, cwd=cwd, env=env)
                await Util.asyncProcWait(proc, cmd)
                outStr = pathlib.Path(tempOutFullfn).read_text()

            if self._verbose > 0:
                print(outStr)

            return outStr
        except subprocess.CalledProcessError as e:
            if self._verbose > 0:
                print(e.stdout)
            raise


# def run_exe(self, executable_path: str, envs: dict = {}, cwd: str = None):
#     '''
#     Execute exe files inside the wineprefix.
#     executable_path : str
#         full path to the .exe file
#     envs: dict, optional
#         dict of environment variables to pass on the execution
#     cwd: str, optional
#         full path to the working directory
#     '''
#     command = executable_path
#     self.execute(command=command, envs=envs, cwd=cwd)

# def run_msi(self, msi_path: str, envs: dict = {}, cwd: str = None):
#     '''
#     Execute msi files inside the wineprefix.
#     msi_path : str
#         full path to the .msi file
#     envs: dict, optional
#         dict of environment variables to pass on the execution
#     cwd: str, optional
#         full path to the working directory
#     '''
#     command = f"msiexec /i {msi_path}"
#     self.execute(command=command, envs=envs, cwd=cwd)

# def run_bat(self, bat_path: str, envs: dict = {}, cwd: str = None):
#     '''
#     Execute bat files inside the wineprefix.
#     bat_path : str
#         full path to the .bat file
#     envs: dict, optional
#         dict of environment variables to pass on the execution
#     cwd: str, optional
#         full path to the working directory
#     '''
#     command = f"wineconsole cmd /c '{bat_path}'"
#     self.execute(command=command, envs=envs, cwd=cwd)
